//
//  BangoAnalyticsManager.m
//  Bango
//

#import "BangoAnalyticsManager.h"
#import "Profile.h"
#import "Event.h"
#import "EventParameter.h"
#import "BangoConstants.h"
#import "BangoConfigConstants.h"
#import "BangoSessionEvents.h"
#import "CoreDataHelper.h"
#import "SBJsonWriter.h"
#import "ASIHTTPRequest.h"
#import "SBJsonParser.h"
#import "BangoUtils.h"
#import "Reachability.h"
#import "BangoConstants.h"
#import "BangoFieldNames.h"
#import "TransmissionModes.h"

// Custom Logger for BangoAnalyticsManager
#define BAMLog(fmt,...) [self debug:__FILE__ lineNumber:__LINE__ format:(fmt),##__VA_ARGS__]

static BangoAnalyticsManager *sharedMyManager = nil;

@implementation BangoAnalyticsManager

// db
@synthesize managedObjectContext;

// fields
@synthesize profileFieldsDictionary, eventFieldsDictionary;

// config options
@synthesize applicationChannel, applicationType, applicationVersion, continueSessionSeconds, findLocation, logEnabled, automaticDataTransmission, postUrl, userId, ipAddress;

// device can make mobile phone calls
@synthesize deviceIsMobile;

// useragent
@synthesize userAgent;

// core location
@synthesize locationManager;

// transmission
@synthesize transmitting, transmissionMode, transmittingProfile, responseData;

// debug log
@synthesize debugLog;

#pragma mark -
#pragma mark Memory Management

// dealloc
-(void) dealloc
{
	BAMLog(@"> %s", __PRETTY_FUNCTION__);	
	[locationManager release];
	[super dealloc];
}

#pragma mark Singleton Methods
+ (id)sharedManager {
	@synchronized(self) {
		if(sharedMyManager == nil) {
			sharedMyManager = [[super allocWithZone:NULL] init];
		}
	}
	return sharedMyManager;
}

+ (id)allocWithZone:(NSZone *)zone {
	return [[self sharedManager] retain];
}

- (id)copyWithZone:(NSZone *)zone {
	return self;
}

- (id)retain {
	return self;
}

- (unsigned)retainCount {
	return UINT_MAX; //denotes an object that cannot be released
}

- (void)release {
	// never release
}

- (id)autorelease {
	return self;
}

// init method
- (id) init {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	if(self ==[super init]) {
		self.userAgent = [self userAgentString];
		
		// set continue session seconds to -1, if the number is 0 or greater than we will store it in the profile
		self.continueSessionSeconds = -1;
		
		// set default for automatic data transmission to YES
		self.automaticDataTransmission = YES;
		
	}
	return self;
}

// todo enforce loading managed object context
// init method with managed object context
- (id) initWithManagedObjectContext:(NSManagedObjectContext*)moContext {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	if(self ==[super init]) {
		
		//initialise the managed object contxt
		managedObjectContext = moContext;
	}
	return self;
}



#pragma mark -
#pragma mark Bango Analytics API Methods


/*
 * Session Methods: These are to do with the Session Profile. i.e. starting a session (creating a new profile) and ending a session (attempting to transmit
 * the profile info).
 */

// This method initiates logging, this must be called after the configuration methods but before any other logging or any other session methods.
-(void)onStartSessionWithApplicationId :(NSString*)applicationId {
	[self onStartSessionWithApplicationId :applicationId eventParameters:nil eventDetail:nil eventValue:nil];
}

// This method initiates logging, this must be called after the configuration methods but before any other logging or any other session methods.
-(void)onStartSessionWithApplicationId :(NSString*)applicationId eventParameters:(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@".");
	BAMLog(@".");
	BAMLog(@".");
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	BAMLog(@"session started with applicationId: %@", applicationId);
	BAMLog(@"context %@", managedObjectContext);
	
	if(!self.logEnabled) {
		BAMLog(@"loggin has been disabled - not creating profile");
		return;
	}
	
	// start core location if required and if it is not already started
	if(findLocation == YES) {
		BAMLog(@"onStartSession with findLocation == YES");
		if(self.locationManager == nil) {
			BAMLog(@"initialising locationManager");
			self.locationManager = [[CLLocationManager alloc] init]; 
			self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters; 
			self.locationManager.delegate = self;
			[self.locationManager startUpdatingLocation];
		} else {
			BAMLog(@"locationManager already initialised");
		}
	} else {
		BAMLog(@"onStartSesion with findLocation == NO");
	}
	

	// Step 1 Create the profile
	Profile *profile = (Profile *)[NSEntityDescription insertNewObjectForEntityForName:@"Profile" inManagedObjectContext:managedObjectContext];
	
	profile.created = [NSDate date];
	
	// set user configurable values here
	
	// ApplicationChannel
	if(self.applicationChannel != nil) {
		BAMLog(@"applicationChannel provided: %@", self.applicationChannel);
		profile.applicationChannel = self.applicationChannel;
	}
	
	// ApplicationType
	if(self.applicationType != nil) {
		BAMLog(@"applicationType provided: %@", self.applicationType);
		profile.applicationType = self.applicationType;
	}
	
	// ApplicationVersion
	if(self.applicationVersion != nil) {
		BAMLog(@"ovverriding applicationVersion with user configurated value %@", self.applicationVersion);
		profile.applicationVersion = self.applicationVersion;
	} else{
		NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
		BAMLog(@"applicationVersion not overriden - using actual app version: %@", version);
		profile.applicationVersion = version;
	} 
	
	// session continue time we accept any value of 0 or greater as a valid value to ovveride the default
	// this valus is intialised to -1 in init.
	if(self.continueSessionSeconds >= 0) {
		BAMLog(@"continue session seconds provided %i", self.continueSessionSeconds);
		profile.sessionRestartTime = [NSNumber numberWithInt:self.continueSessionSeconds];
	} else {
		BAMLog(@"continueSessionSeconds not provided. Not storing.");
	}
	
	// IPAddress
	if(self.ipAddress != nil) {
		BAMLog(@"ipAddress provided: %@", self.ipAddress);
		profile.ipAddress = self.ipAddress;
	}
	
	// user id
	if(self.userId != nil) {
		BAMLog(@"ovverriding userId with user configurated value %@", self.userId);
		profile.userId = self.userId;
	} else{
		NSString *udid = [UIDevice currentDevice].uniqueIdentifier;
		BAMLog(@"userId not overriden - using actual udid : %@", udid);
		profile.userId = udid;
	} 
	
	// set non configurable profile values here
	profile.applicationKey = applicationId;
	profile.sdkType = [NSNumber numberWithInt:SDK_TYPE];
	profile.sdkVersion = [NSNumber numberWithDouble:SDK_VERSION];
	profile.model = [UIDevice currentDevice].model;
	profile.device =@"Apple";
	
	// conection type
	NSString *connectionType;
	if ([[Reachability reachabilityForInternetConnection] currentReachabilityStatus] == kReachableViaWiFi) {
		connectionType=CONNECTION_TYPE_WIFI;
	} else if ([[Reachability reachabilityForInternetConnection] currentReachabilityStatus] == kReachableViaWWAN) {
		connectionType=CONNECTION_TYPE_OPERATOR;
	} else {
		connectionType=CONNECTION_TYPE_OTHER;
	}
	profile.connectionType =connectionType;
	BAMLog(@"connectionType: %@", connectionType);
	
	// location
	CLLocation *curPos = self.locationManager.location; 
	double latitude = curPos.coordinate.latitude;
	double longitude = curPos.coordinate.longitude;
	double altitude = curPos.altitude;
	
	// todo should we take average of horizontal and vertical accuracy?
	double accuracy = curPos.horizontalAccuracy;
	
	if(self.findLocation) {
		BAMLog(@"setting location");
		// todo the location will probably not have been picked up this quickly anyway so we need a plan to set it once its kicked in
		profile.locationLatitude = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:latitude]];
		profile.locationLongitude = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:longitude]];
		profile.locationAccuracy = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:accuracy]];
		profile.locationAltitude = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:altitude]];
	} else {
		BAMLog(@"not checking location");
	}
	
	NSError *error = nil;
	
	// Step 2 Create the event
	Event *event = [self createEventForProfile:profile withEventName:SESSION_START eventDetail:eventDetail eventValue:eventValue eventParameters:eventParameters];
	
	// The event might be nil if logEnabled is set to NO - so we should not try and assign it to this event
	// event though technically we should not be creating profiles when we are not logging events.
	if(event != nil) {
		// Step 3 Add the event to the Profile
		[profile addEventsObject:event];
	}

	// save the changes
	if (![profile.managedObjectContext save:&error]) {
		// Handle error
		BAMLog(@"error creating profile in onStartSession %@, %@", error, [error userInfo]);
	}
	
	// transmitt all except for latest created
	if(self.automaticDataTransmission == YES) {
		BAMLog(@"automaticDataTransmission enabled. Transmitting all excluding latest.");
		[self commenceTransmissionWithMode:TRANSMIT_EXCLUDE_LATEST];
	}
	
}

// creates an event for a profile
// can be used to create any type of event
-(Event*) createEventForProfile :(Profile*)profile withEventName:(NSString *)eventName eventDetail:(NSString *)eventDetail eventValue:(NSString *)eventValue 
				 eventParameters:(NSDictionary*)eventParameters {
	BAMLog(@".");
	BAMLog(@".");
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	return [self createEventForProfile:profile withEventName:eventName eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:(NSDictionary *)eventParameters];
}

// creates an event for a profile
// can be used to create any type of event
-(Event*) createEventForProfile :(Profile*)profile withEventName:(NSString *)eventName eventDetail:(NSString *)eventDetail eventValue:(NSString*)eventValue
				 eventErrorLevel:(NSNumber*)eventErrorLevel eventErrorId:(NSString*)eventErrorId 
				 eventParameters:(NSDictionary*)eventParameters {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	BAMLog(@"\neventName: %@ \neventDetail %@ \neventValue %@ \neventErrorLevel %@ \neventErrorId %@ \neventParameters %@", eventName, eventDetail, eventValue, eventErrorLevel, eventErrorId, eventParameters);
	
	Event *event = nil;
	
	if(self.logEnabled == YES) {
		
		event = (Event *)[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:managedObjectContext];
		
		// set event values here
		event.eventName = eventName;
		
		if(eventDetail != nil) {
			event.eventDetail = eventDetail;
		}
		
		if(eventValue != nil) {
			event.eventValue = eventValue;
		}
		
		if(eventErrorId != nil) {
			event.errorId = eventErrorId;
		}
		
		if(eventErrorLevel != nil) {
			event.errorLevel = eventErrorLevel;
		}
		
		// store event time
		event.eventTime = [NSDate date];
		
		// add the event parameters here
		// from the event parameters map
		if(eventParameters != nil) {
			
			NSArray *keys = [eventParameters allKeys];
			
			NSString *key;
			NSString *value;
			EventParameter *eventParameter;
			for (int i=0; i<[keys count]; i++){
				key = [keys objectAtIndex:i];
				value = [eventParameters objectForKey:key];
				BAMLog(@"event parameter: %@, %@", key, value);
				eventParameter = [self createEventParameterForEvent:event withEventName:key eventValue:value];
				
				// add the parameter to the event
				[event addEventParametersObject:eventParameter];
			}
		}
	} else {
		BAMLog(@"event logging is disabled - not logging event");
	}
	
	return event;
}

// Creates an event parameter for an event
-(EventParameter*) createEventParameterForEvent :(Event*)event withEventName:(NSString *)eventName eventValue:(NSString *)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	EventParameter *eventParameter = (EventParameter *)[NSEntityDescription insertNewObjectForEntityForName:@"EventParameter" inManagedObjectContext:managedObjectContext];
	eventParameter.eventName = eventName;
	eventParameter.eventValue = eventValue;
	return eventParameter;
}

// method which creates an event for the current profile
// can be used to create any kind of event
-(void)createEventForCurrentProfile :(NSString *)eventName eventDetail:(NSString *)eventDetail eventValue:(NSString*)eventValue
					 eventErrorLevel:(NSNumber*)eventErrorLevel eventErrorId:(NSString*)eventErrorId 
					 eventParameters:(NSDictionary*)eventParameters {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	// get the current profile
	Profile *profile = [CoreDataHelper getMostRecentProfile:managedObjectContext];
	
	if(profile == nil) {
		// todo create a new profile
		BAMLog(@"No current profile!");
		BAMLog(@"Profile has probably not been intialised on app launch.");
		
		// throw exception for production release?
		// helps identify errors with integration quickly
		// we should log a bango error - but we can't log an error if we have no profile...
		[NSException raise:@"No Profile Created." format:@"event creation should not be attempted until after onStartSession has been called"];
	}
	
	NSError *error = nil;
	
	// Step 2 Create the event
	Event *event = [self createEventForProfile:profile withEventName:eventName eventDetail:eventDetail eventValue:eventValue 
							   eventErrorLevel:eventErrorLevel eventErrorId:eventErrorId  eventParameters:eventParameters];
	
	// Step 3 Add the event to the Profile
	[profile addEventsObject:event];
	
	// save the changes
	if (![profile.managedObjectContext save:&error]) {
		// Handle error
		BAMLog(@"error ending session %@, %@", error, [error userInfo]);
	}
}

// method which creates an event for the current profile
// can be used to create any kind of event
-(void)onEvent :(NSString *)eventName eventDetail:(NSString *)eventDetail eventValue:(NSString*)eventValue eventParameters:(NSDictionary*)eventParameters {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self createEventForCurrentProfile:eventName eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:eventParameters];
}

// This method indicates that the session has ended and will initiate the transmitting of data.
-(void)onEndSession {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onEndSessionWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method indicates that the session has ended and will initiate the transmitting of data.
-(void)onEndSessionWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	[self createEventForCurrentProfile:SESSION_END eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:eventParameters];
}

// This method indicates that the session has entered an idle state, for example the application has lost focus.
-(void)onIdle {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onIdleWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method indicates that the session has entered an idle state, for example the application has lost focus.
-(void)onIdleWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	[self createEventForCurrentProfile:SESSION_IDLE_START eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:eventParameters];
}

// This method indicates that the session has resumed after an idle state, for example the application has gained focus after losing focus.
-(void)onResume {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onResumeWithEventParameters :nil eventDetail:nil eventValue:nil];	
}	

// This method indicates that the session has resumed after an idle state, for example the application has gained focus after losing focus.
-(void)onResumeWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	[self createEventForCurrentProfile:SESSION_IDLE_END eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:eventParameters];
	
	
	// transmit all profiles but do not delete last profile - delete all events in last profile.
	if(self.automaticDataTransmission == YES) {
		BAMLog(@"automaticDataTransmission enabled. Transmitting all data - and retaining latest profile");
		[self commenceTransmissionWithMode:TRASMIT_ALL_RETAIN_LATEST];	
	}
}

/*
 * Logging Methods: These are to do with logging events such as video start and stop
 */

// This method logs an onDemandVideoStart event.
-(void)onDemandVideoStart {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onDemandVideoStartWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method logs an onDemandVideoStart event.
-(void)onDemandVideoStartWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	NSMutableDictionary *mutable;
	if(eventParameters == nil) {
		mutable = [[NSMutableDictionary alloc] init];
	} else {
		mutable = [[NSMutableDictionary alloc] initWithDictionary:eventParameters];
	}
	
	[mutable setObject:VIDEO_ONDEMANDPARAMETER forKey:VIDEO_PARAMETERNAME];
	
	[self createEventForCurrentProfile:EVENT_VIDEOSTART eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:mutable];
	
	[mutable release];
}

// This method logs an onDemandVideoStop event.
-(void)onDemandVideoStop {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	[self onDemandVideoStopWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method logs an onDemandVideoStop event.
-(void)onDemandVideoStopWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	
	NSMutableDictionary *mutable;
	if(eventParameters == nil) {
		mutable = [[NSMutableDictionary alloc] init];
	} else {
		mutable = [[NSMutableDictionary alloc] initWithDictionary:eventParameters];
	}
	
	[mutable setObject:VIDEO_ONDEMANDPARAMETER forKey:VIDEO_PARAMETERNAME];
	
	[self createEventForCurrentProfile:EVENT_VIDEOSTOP eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:mutable];
	
	[mutable release];
	
}

// This method logs an onLiveVideoStart event.
-(void)onLiveVideoStart {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onLiveVideoStartWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method logs an onLiveVideoStart event.
-(void)onLiveVideoStartWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	NSMutableDictionary *mutable;
	if(eventParameters == nil) {
		mutable = [[NSMutableDictionary alloc] init];
	} else {
		mutable = [[NSMutableDictionary alloc] initWithDictionary:eventParameters];
	}
	
	[mutable setObject:VIDEO_LIVEPARAMETER forKey:VIDEO_PARAMETERNAME];
	
	[self createEventForCurrentProfile:EVENT_VIDEOSTART eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:mutable];
	
	[mutable release];
}

// This method logs an onLiveVideoStop event.
-(void)onLiveVideoStop {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	[self onLiveVideoStopWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method logs an onLiveVideoStop event.
-(void)onLiveVideoStopWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	
	NSMutableDictionary *mutable;
	if(eventParameters == nil) {
		mutable = [[NSMutableDictionary alloc] init];
	} else {
		mutable = [[NSMutableDictionary alloc] initWithDictionary:eventParameters];
	}
	
	[mutable setObject:VIDEO_LIVEPARAMETER forKey:VIDEO_PARAMETERNAME];
	
	
	[self createEventForCurrentProfile:EVENT_VIDEOSTOP eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:mutable];
	
	[mutable release];
}

// This method logs an onPageView event.
-(void)onPageView {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onPageViewWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method logs an onPageView event.
-(void)onPageViewWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self createEventForCurrentProfile:EVENT_PAGEVIEW eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:eventParameters];
}

// This method logs a search event.
-(void)onSearch {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onSearchWithEventParameters :nil eventDetail:nil eventValue:nil];
}

// This method logs a search event.
-(void)onSearchWithEventParameters :(NSDictionary*)eventParameters eventDetail:(NSString*)eventDetail eventValue:(NSString*)eventValue {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self createEventForCurrentProfile:EVENT_SEARCH eventDetail:eventDetail eventValue:eventValue eventErrorLevel:nil eventErrorId:nil eventParameters:eventParameters];
}

// This method logs an error event.
-(void)onError {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	[self onErrorWithEventParameters :nil message:nil eventClass:nil eventErrorLevel:[NSNumber numberWithInt:ERROR_USER] eventErrorId:nil];
}

// This method logs an error event.
-(void)onErrorWithEventParameters :(NSDictionary*)eventParameters message:(NSString*)message eventClass:(NSString*)eventClass eventErrorId:(NSString*)eventErrorId {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	[self onErrorWithEventParameters:eventParameters message:message eventClass:eventClass eventErrorId:eventErrorId];
}

// This method logs an error event.
-(void)onErrorWithEventParameters :(NSDictionary*)eventParameters message:(NSString*)message eventClass:(NSString*)eventClass 
				   eventErrorLevel:(NSNumber*)eventErrorLevel eventErrorId:(NSString*)eventErrorId {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	[self createEventForCurrentProfile:EVENT_ERROR eventDetail:message eventValue:eventClass eventErrorLevel:eventErrorLevel eventErrorId:eventErrorId eventParameters:eventParameters];
}


#pragma mark -
#pragma mark UserAgent Detection Methods

/////////////////////////////////////////
// for getting the useragent - adapted from handy method by Björn Sållarp
// http://blog.sallarp.com/iphone-ipad-get-user-agent-for-uiwebview/#more-1003
//
-(NSString*)userAgentString
{
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	webView = [[UIWebView alloc] init];
	webView.delegate = self;
	[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.bango.com"]]];
	
	// Wait for the web view to load our bogus request and give us the secret user agent.
	while (self.userAgent == nil) 
	{
		// This executes another run loop. 
		[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
	}
	
	return self.userAgent;
}

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	self.userAgent = [request valueForHTTPHeaderField:@"User-Agent"];
	
	BAMLog(@"setting userAgent: %@", self.userAgent);
	
	// Return no, we don't care about executing an actual request.
	return NO;
}

#pragma mark -
#pragma mark AnalyticsTransmission Methods

// commense transmission with the specified mode
-(void)commenceTransmissionWithMode:(NSString*)mode {
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	BAMLog(@"previous mode: %@", self.transmissionMode);
	BAMLog(@"new mode: %@",mode);
	self.transmissionMode = mode;
	
	// call transmit All
    [self transmitAll];
}
	 

// kicks off process to transmit all outstanding analytics data to bango
-(void)transmitAll{
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	
	BAMLog(@"transmissionMode: %@",self.transmissionMode);
	
	// don't attempt to transmit if we are already transmitting
	if(transmitting == YES) {
		BAMLog(@"already transmitting - attempt made to transmit whilst already transmitting - aborting transmission");
		return;
	}
	
	
	NSMutableArray *profiles = [CoreDataHelper getProfilesByDateAsc:managedObjectContext];
	
	int totalProfiles = [profiles count];
	
	BAMLog(@"totalProfiles: %i", totalProfiles);
	
	if(totalProfiles >= 1) {
		
		// handle this based on transmission mode
		bool transmitOldestProfile = YES;
		
		if(self.transmissionMode == TRANSMIT_EXCLUDE_LATEST) {
			// check that this is not the only profile remaining
			if (totalProfiles < 2) {
				BAMLog(@"profile ready for transmission is latest. transmissionMode set to exclude latest.");
				transmitOldestProfile = NO;
			} else {
				BAMLog(@"profile ready for transmission is not latest. transmissionMode set to exclude latest.");
			}
		}
		if(transmitOldestProfile == YES) {
			BAMLog(@"transmitting profile");
			[self transmitProfile:[profiles objectAtIndex:0]];
		} else {
			BAMLog(@"not transmiting profile");
			transmissionMode = nil;
		}
	} else {
		BAMLog(@"no profiles to transmit - terminating profile transmission");
		transmissionMode = nil;
	}
}

// transmits analytics data to bango for specific profile
-(void)transmitProfile:(Profile*)profile {
	BAMLog(@">", __PRETTY_FUNCTION__);
	BAMLog(@"profile: %@", profile.created);
	
	// BAMLog(@"profile %@", profile);
	
		
	if(profile.events == nil || [profile.events count] == 0) {
		// Check if Profile contains events - if it does not contain events then we should not transmit it
		// We can be left with an eventless profile immediately after performing a transmit all retain latest.
		// In addition, if a new profile has been created after we have been left with an eventless profile
		// (e.g. due to app crash before new events being added) then we can delete such a profile.
		// We just need to ensure that there is a current profile to place events in (i.e. this is not the
		// current profile).
			
		BAMLog(@"Attempting to transmit profile with no events");
		BAMLog(@"Skipping Profile Transmission");
		
		Profile *mostRecentProfile = [CoreDataHelper getMostRecentProfile:managedObjectContext];

		if(profile == mostRecentProfile) {
			BAMLog(@"profile is the current profile. Not deleting it");
		} else {
			BAMLog(@"profile is not the current profile. Deleting it");
			[self deleteProfile:profile];
		}
		
		return;
	}
	
	// Set the profile we are transmitting
	self.transmittingProfile = profile;
	
	SBJsonWriter *writer = [SBJsonWriter new];
	writer.sortKeys = YES;
	
	// add spacing / tabs etc - best not to do this in production
	writer.humanReadable = YES;
	
	NSError *error = nil;

	// create JSON String of the profile and the events and their event params
	NSString *profileJson = [writer stringWithObject:profile error:&error];
	BAMLog(@"profileJson %@", profileJson);

	[writer release];
	
	BAMLog(@"json error %@", error);
	
	NSString *urlString;
	if(self.postUrl != nil) {
		BAMLog(@"postUrl overriden in userConfig");
		urlString = self.postUrl;
	} else {
		BAMLog(@"using default postUrl", SETTING_POST_URL);
		urlString = SETTING_POST_URL;
	}
	
	BAMLog(@"transmission URL: %@", urlString);
	
	NSURL *url = [NSURL URLWithString: urlString];
	
	// Note: there is a delay on ASI data being release since it is autoreleased
	// we can get rid of this by using our own implementation of asynchronous url posts.
	// Have emailed ASI for comments but for now we'll just not use ASI.
	/*
	ASIHTTPRequest *request = [[ASIHTTPRequest requestWithURL:url] retain];
    [request setDelegate:self];
	[request setRequestMethod:@"POST"];
	[request setDidFinishSelector:@selector(requestDone:)];
	[request setDidFailSelector:@selector(requestWentWrong:)];
	[request addRequestHeader: @"Content-Type" value:@"application/json"];
	[request addRequestHeader: @"User-Agent" value:userAgent];
	
    [request appendPostData:[profileJson
							 dataUsingEncoding:NSUTF8StringEncoding]];
	
	self.transmitting = YES;
    [request startAsynchronous]; 
	 */
	
	NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url 
										 cachePolicy:NSURLRequestReloadIgnoringCacheData 
									 timeoutInterval:30.0];
	[req setValue:@"application/json" forHTTPHeaderField: @"Content-Type"];
	[req setValue:userAgent forHTTPHeaderField:@"User-Agent"];
	[req setHTTPMethod:@"POST"];
	[req setHTTPBody:[profileJson dataUsingEncoding:NSUTF8StringEncoding]];
	
	// start the async request
	[NSURLConnection connectionWithRequest:req delegate:self];
	
	// mark that we are transmitting
	self.transmitting = YES;
}

//
//  Received at the start of the response from the server.  This may get called multiple times in certain redirect scenerios.
// 
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
	
	NSLog(@"Received Response");
	
	if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
		NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
		int status = [httpResponse statusCode];
		
		if (!((status >= 200) && (status < 300))) {
			BAMLog(@"Connection failed with status %@", status);
			self.transmittingProfile = nil;
			self.transmitting = NO;
			BAMLog(@"transmittingProfile data has been reset - aborting all transmissions");	
		} else {
			// make the working space for the REST data buffer.  This could also be a file if you want to reduce the RAM footprint
			[self.responseData release];
			self.responseData = [[NSMutableData alloc] initWithCapacity:1024];
		}
	}
}

//
//  Can be called multiple times with chunks of the transfer
//
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
	
	NSLog(@"Received Response");
	
	[self.responseData appendData:data];
}

//
// Called once at the end of the request
//
- (void)connectionDidFinishLoading:(NSURLConnection *)conn {
	NSString *response = [[NSString alloc]  initWithBytes:[self.responseData bytes]
												  length:[self.responseData length] encoding: NSUTF8StringEncoding];
	
	BAMLog(@".");
	BAMLog(@".");
	BAMLog(@"%s",__PRETTY_FUNCTION__);
	BAMLog(@"transmittingProfile %@", self.transmittingProfile.created);
	
	// if request starts with OK then store that we have transmitted the data successfully
	// NB. The two responses indidcating success are OK and OK_WITH_ERRORS
	if([response hasPrefix:POST_RESPONSE_SUCCESS]) {
		BAMLog(@"Profile Transmission Successful - determining delete logic");
		
		BAMLog(@"transmissionMode %@", self.transmissionMode);
		
		if(self.transmissionMode == TRANSMIT_ALL || self.transmissionMode == TRANSMIT_EXCLUDE_LATEST) {
			// delete the profile which has just been transmitted as both transmission modes require full profile deletion
			BAMLog(@"deleting profile %@ ", self.transmittingProfile.created);
			
			// attempt to delete and then send next profile
			bool success = [self deleteProfile:self.transmittingProfile];
			if(success == YES) {
				BAMLog(@"profile deleted succesfully - reseting transmittngData and continuing with further transmissions");
				// can kick off transmission
				self.transmittingProfile = nil;
				self.transmitting = NO;
				[self transmitAll];
			} else {
				BAMLog(@"problem deleting transmitting profile - reseting transmittngData but aborting further transmissions");
				self.transmittingProfile = nil;
				self.transmitting = NO;
			}
			
		} else if(self.transmissionMode == TRASMIT_ALL_RETAIN_LATEST) {
			// get the current profile and see if it is the latest or not
			// if it is not the latest we can delete
			// if it is the latest then we only want to delete the events
			Profile *currentProfile = [CoreDataHelper getMostRecentProfile:managedObjectContext];
			
			// compare the profiles by looking at the dates
			NSDate *currentProfileDate = currentProfile.created;
			NSDate *transmittingProfileDate = self.transmittingProfile.created;
			
			BAMLog(@"currentProfileDate: %@", currentProfileDate);
			BAMLog(@"transmittingProfileDate: %@", transmittingProfileDate);
			
			bool profileIsLatest = [currentProfileDate isEqualToDate:transmittingProfileDate];
			
			if(profileIsLatest == YES) {
				BAMLog(@"profile is latest - should only delete events");
				
				bool success = [self deleteEventsInProfile:self.transmittingProfile];
				
				self.transmittingProfile = nil;
				self.transmitting = NO;
				
				if(success == YES) {
					BAMLog(@"events were deleted successfully. Resetting transmittingData values. This was the last profile. No further action required");
				} else {
					BAMLog(@"events were not deleted successfully. They will be potentially transmitted again. This was the last profile. No further action required");
				}
				
			} else {
				BAMLog(@"profile is not latest - can delete");
				// attempt to delete and then send next profile
				bool success = [self deleteProfile:self.transmittingProfile];
				if(success == YES) {
					BAMLog(@"profile deleted succesfully - reseting transmittngData and continuing with further transmissions");
					// can kick off transmission
					self.transmittingProfile = nil;
					self.transmitting = NO;
					[self transmitAll];
				} else {
					BAMLog(@"problem deleting transmitting profile - reseting transmittngData but aborting further transmissions");
					self.transmittingProfile = nil;
					self.transmitting = NO;
				}
			}
		} else {
			BAMLog(@"unexpected transmissionMode %@", self.transmissionMode);
			// log internal error
			self.transmitting = NO;
			
		}
	} else {
		BAMLog(@"request was not posted successfully");
		self.transmitting = NO;
	}	
}

//
// Something bad happened before the response started
//
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
	
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	BAMLog(@"error %@ occured whilst transmittingProfile: %@", error, self.transmittingProfile);
	
	self.transmittingProfile = nil;
	self.transmitting = NO;
	self.responseData = nil;
	
	BAMLog(@"transmittingProfile data has been reset - aborting all transmissions");	
}

// handles sucessful request to Bango
- (void)requestDone:(ASIHTTPRequest *)request
{
	BAMLog(@".");
	BAMLog(@".");
	BAMLog(@"%s",__PRETTY_FUNCTION__);
	BAMLog(@"transmittingProfile %@", self.transmittingProfile.created);
	
	NSString *response = [request responseString];
	BAMLog(@"response: [%@]", response);
	
	// if request starts with OK then store that we have transmitted the data successfully
	// NB. The two responses indidcating success are OK and OK_WITH_ERRORS
	if([response hasPrefix:POST_RESPONSE_SUCCESS]) {
		BAMLog(@"Profile Transmission Successful - determining delete logic");
		
		BAMLog(@"transmissionMode %@", self.transmissionMode);
		
		if(self.transmissionMode == TRANSMIT_ALL || self.transmissionMode == TRANSMIT_EXCLUDE_LATEST) {
			// delete the profile which has just been transmitted as both transmission modes require full profile deletion
			BAMLog(@"deleting profile %@ ", self.transmittingProfile.created);
			
			// attempt to delete and then send next profile
			bool success = [self deleteProfile:self.transmittingProfile];
			if(success == YES) {
				BAMLog(@"profile deleted succesfully - reseting transmittngData and continuing with further transmissions");
				// can kick off transmission
				self.transmittingProfile = nil;
				self.transmitting = NO;
				[self transmitAll];
			} else {
				BAMLog(@"problem deleting transmitting profile - reseting transmittngData but aborting further transmissions");
				self.transmittingProfile = nil;
				self.transmitting = NO;
			}
			
		} else if(self.transmissionMode == TRASMIT_ALL_RETAIN_LATEST) {
			// get the current profile and see if it is the latest or not
			// if it is not the latest we can delete
			// if it is the latest then we only want to delete the events
			Profile *currentProfile = [CoreDataHelper getMostRecentProfile:managedObjectContext];
			
			// compare the profiles by looking at the dates
			NSDate *currentProfileDate = currentProfile.created;
			NSDate *transmittingProfileDate = self.transmittingProfile.created;
			
			BAMLog(@"currentProfileDate: %@", currentProfileDate);
			BAMLog(@"transmittingProfileDate: %@", transmittingProfileDate);
			
			bool profileIsLatest = [currentProfileDate isEqualToDate:transmittingProfileDate];
			
			if(profileIsLatest == YES) {
				BAMLog(@"profile is latest - should only delete events");
				
				bool success = [self deleteEventsInProfile:self.transmittingProfile];
				
				self.transmittingProfile = nil;
				self.transmitting = NO;
				 
				if(success == YES) {
					BAMLog(@"events were deleted successfully. Resetting transmittingData values. This was the last profile. No further action required");
				} else {
					BAMLog(@"events were not deleted successfully. They will be potentially transmitted again. This was the last profile. No further action required");
				}
				
			} else {
				BAMLog(@"profile is not latest - can delete");
				// attempt to delete and then send next profile
				bool success = [self deleteProfile:self.transmittingProfile];
				if(success == YES) {
					BAMLog(@"profile deleted succesfully - reseting transmittngData and continuing with further transmissions");
					// can kick off transmission
					self.transmittingProfile = nil;
					self.transmitting = NO;
					[self transmitAll];
				} else {
					BAMLog(@"problem deleting transmitting profile - reseting transmittngData but aborting further transmissions");
					self.transmittingProfile = nil;
					self.transmitting = NO;
				}
			}
		} else {
			BAMLog(@"unexpected transmissionMode %@", self.transmissionMode);
			// log internal error
		}
	} else {
		BAMLog(@"request was not posted successfully");
		self.transmitting = NO;
	}
}

// handles failed request to Bango
- (void)requestWentWrong:(ASIHTTPRequest *)request
{
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	NSError *error = [request error];
	BAMLog(@"error %@ occured whilst transmittingProfile: %@", error, self.transmittingProfile);
	
	self.transmittingProfile = nil;
	self.transmitting = NO;
	
	BAMLog(@"transmittingProfile data has been reset - aborting all transmissions");
	
	// todo either try and send this profile again, or try with other profiles.
	// this might result in profiles being sent in the wrong order and thus
	// data appearing in bango analytics front end strangely
}


#pragma mark -
#pragma mark CoreData warehousing

// deletes the specified profile
-(bool)deleteProfile:(Profile*)profile {
	BAMLog(@".");
	BAMLog(@"%s",__PRETTY_FUNCTION__);
	BAMLog(@"profile to delete %@", profile.created);
	NSError *error = nil;
	bool success = YES;
	
	[managedObjectContext deleteObject:profile];
	if (![profile.managedObjectContext save:&error]) {
		// Handle error
		BAMLog(@"error deleting profile: %@, transmittingProfile %@, transmissionMode %@, error: %@, userInfo%@", profile.created, self.transmissionMode, self.transmittingProfile.created, error, [error userInfo]);
		// todo log api error
		success = NO;
	} else {
		BAMLog(@"profile successfully deleted");
		// set to nil
		self.transmittingProfile = nil;
	}	
	return success;
}

// deletes the events in the specified profile
-(bool)deleteEventsInProfile:(Profile*)profile {
	BAMLog(@".");
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	BAMLog(@"profile %@", profile.created);
	
	// remove all the events
	profile.events = nil;
	
	NSError *error = nil;
	bool success = YES;
	if (![profile.managedObjectContext save:&error]) {
		// Handle error
		BAMLog(@"error deleting events for profile: %@, transmittingProfile %@, transmissionMode %@, error: %@, userInfo%@", profile, self.transmissionMode, self.transmittingProfile, error, [error userInfo]);
		// todo log api error
		success = NO;
	} else {
		BAMLog(@"successfull deletion of events for profile: %@, transmittingProfile:", profile.created, self.transmittingProfile.created);
	}	
	return success;
}
								

#pragma mark -
#pragma mark Core Location Delegate Methods

// we implement this delegate method
- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
    BAMLog(@"Location Updated: %@", [newLocation description]);
	
	// update the current profile with this location
	
	// get the current profile
	Profile *profile = [CoreDataHelper getMostRecentProfile:managedObjectContext];
	
	if(profile == nil) {
		// todo create a new profile
		BAMLog(@"no current profile - creating one");
		BAMLog(@"*** Check if we should implement this ***");
		
		// do not throw exception for production releases
		// we should log a bango error
		[NSException raise:@"No Profile Created." format:@"location updated but no current profile to update event data with"];
	}
	
	BAMLog(@"location updated");
	BAMLog(@"old location lat:%@, long:%@ alt:%@, accuracy:%@", profile.locationLatitude, profile.locationLongitude, profile.locationAltitude, profile.locationAccuracy);
	
	profile.locationLatitude = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:newLocation.coordinate.latitude]];
	profile.locationLongitude = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:newLocation.coordinate.longitude]];
	profile.locationAltitude = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:newLocation.altitude]];
	profile.locationAccuracy = [NSNumber numberWithDouble:[BangoUtils getRoundedLocation:newLocation.horizontalAccuracy]];
	
	NSError *error = nil;
		
	// save the changes
	if (![profile.managedObjectContext save:&error]) {
		// Handle error
		BAMLog(@"error updating location data on profile %@ %@, %@", profile, error, [error userInfo]);
	}
	
	BAMLog(@"new location lat:%@, long:%@ alt:%@, accuracy%@", profile.locationLatitude, profile.locationLongitude, profile.locationAltitude, profile.locationAccuracy);
	
	// we can now shut down the core location manager
	[self.locationManager stopUpdatingLocation];
}

- (void)locationManager:(CLLocationManager *)manager
	   didFailWithError:(NSError *)error
{
	BAMLog(@"> %s", __PRETTY_FUNCTION__);
	BAMLog(@"Error: %@", [error description]);
	// we can now shut down the core location manager
	[self.locationManager stopUpdatingLocation];
}

#pragma mark -
#pragma mark Custom Debugger

// logs to debug
// based on method described by Agent M here http://borkware.com/rants/agentm/mlog/
-(void)debug:(char*)file lineNumber:(int)lineNumber format:(NSString *)format, ... {
	
	if(self.debugLog == YES) {
		{
			va_list ap;
			NSString *print,*fileName;
			va_start(ap,format);
			fileName=[[NSString alloc] initWithBytes:file 
											  length:strlen(file) 
											encoding:NSUTF8StringEncoding];
			print=[[NSString alloc] initWithFormat:format arguments:ap];
			va_end(ap);
			//NSLog handles synchronization issues
			NSLog(@"%s:%d %@",[[fileName lastPathComponent] UTF8String], lineNumber,print);
			[print release];
			[fileName release];
			return;
		}
	}
}


@end